更多案例

最佳实践

  • 官方推荐的最佳实践
  1. 保持 Advisor 功能单一化以提升模块性。
  2. 必要时通过 adviseContext 在 Advisor 间共享状态。
  3. 同时实现流式与非流式版本以获得最佳灵活性。
  4. 谨慎规划 Advisor 链顺序以确保数据流正确。
  • 按照上面几点,写了三个案例记录在下面。

案例1:LogExampleAdvisor

需求

  • 编写一个Log案例,将每次请求AI的提示词和上下文,以及AI的响应文本和响应后的上下文输出到日志。
  • 顺序在最后,但必须在AI交互的Advisor前一个

Code

  • 创建一个排序的枚举AdvisorOrders,定义LogExample的排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Getter
    @AllArgsConstructor
    public enum AdvisorOrders {

    // 在最后,但必须小于 Ordered.LOWEST_PRECEDENCE
    LogExample(Ordered.LOWEST_PRECEDENCE - 1),

    ;

    private int order;
    }
  • 创建LogExampleAdvisor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    @Slf4j
    public class LogExampleAdvisor implements CallAdvisor, StreamAdvisor {

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    requestLogger(chatClientRequest);
    ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
    responseLogger(chatClientResponse);
    return chatClientResponse;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    requestLogger(chatClientRequest);
    Flux<ChatClientResponse> chatClientResponseFlux = streamAdvisorChain.nextStream(chatClientRequest);
    // ChatClientMessageAggregator 是SpringAi提供的工具类
    return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponseFlux, this::responseLogger);
    }

    @Override
    public String getName() {
    return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
    return AdvisorOrders.LogExample.getOrder();
    }

    private void requestLogger(ChatClientRequest chatClientRequest) {
    // 输出请求AI的提示词和上下文
    log.info("\nChat client request to AI\nprompt text -> {}\ncontext -> {}", chatClientRequest.prompt().getContents(), ConvertorUtils.toJsonString(chatClientRequest.context()));
    }

    private void responseLogger(ChatClientResponse chatClientResponse) {
    // 输出AI的响应文本和响应后的上下文
    log.info("\nChat client response from AI\noutput text -> {}\ncontext -> {}", chatClientResponse.chatResponse().getResult().getOutput().getText(), chatClientResponse.context());
    }
    }

案例2:SensitiveFilterExampleAdvisor

需求

  • 模拟一个敏感词过滤的案例,在请求之前对User角色的提示词进行敏感词过滤阻断。
  • 如果存在敏感词直接返回,不进行与AI交互。
  • 被过滤后,向上下文添加“敏感词过滤参数”为true
  • 顺序在LogExampleAdvisor之前

Code

  • 定义顺序,增加SensitiveFilterExample(LogExample.order - 1)AdvisorOrders

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Getter
    @AllArgsConstructor
    public enum AdvisorOrders {

    // 在最后,但必须小于 Ordered.LOWEST_PRECEDENCE
    LogExample(Ordered.LOWEST_PRECEDENCE - 1),

    // 在 log 前面
    SensitiveFilterExample(LogExample.order - 1),

    ;

    private int order;
    }
  • 创建一个上下文Key的枚举ContextKeys,用作参数添加是使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public enum ContextKeys {
    // 客户端名称
    ClientName,

    // 敏感词过滤
    SensitiveFilter,

    ;
    }
  • 创建SensitiveFilterExampleAdvisor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    @Slf4j
    public class SensitiveFilterExampleAdvisor implements CallAdvisor, StreamAdvisor {

    // 模拟的敏感词
    private final List<String> words = Lists.newArrayList( "草泥马");

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    if (filter(chatClientRequest)) {

    return createFilterResponse(chatClientRequest);
    }

    return callAdvisorChain.nextCall(chatClientRequest);
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    if (filter(chatClientRequest)) {
    return Flux.just(createFilterResponse(chatClientRequest));
    }

    return streamAdvisorChain.nextStream(chatClientRequest);
    }

    @Override
    public String getName() {
    return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
    return AdvisorOrders.SensitiveFilterExample.getOrder();
    }

    /**
    * 进行过滤
    * @param chatClientRequest
    * @return
    */
    private boolean filter(ChatClientRequest chatClientRequest) {
    List<UserMessage> userMessages = chatClientRequest.prompt().getUserMessages();
    String text = userMessages.stream().map(UserMessage::getText).collect(Collectors.joining());
    boolean filter = words.stream().anyMatch(text::contains);
    if (filter) {
    // 往上下文添加参数
    chatClientRequest.context().put(ContextKeys.SensitiveFilter.name(), true);
    }
    return filter;
    }

    /**
    * 创建过滤的响应
    * @param chatClientRequest
    * @return
    */
    private ChatClientResponse createFilterResponse(ChatClientRequest chatClientRequest) {
    return ChatClientResponse.builder()
    .chatResponse(ChatResponse.builder()
    .generations(List.of(new Generation(new AssistantMessage("用户输入存在敏感词"))))
    .build())
    .context(Map.copyOf(chatClientRequest.context()))
    .build();
    }
    }

案例3:PromptEnhancementExampleAdvisor

需求

  • 模拟一个提示词增强的案例,在请求之前对提示词进行修改增强。
  • 并且模拟将用户信息放入上下文,在增强时使用。
  • 同时加一个条件,在上下文中存在“提示词增强参数”并且是true的情况才进行修改增强。
  • 顺序在SensitiveFilterExampleAdvisor之前

Code

  • 定义顺序,增加PromptEnhancementExample(SensitiveFilterExample.order - 1)AdvisorOrders

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Getter
    @AllArgsConstructor
    public enum AdvisorOrders {

    // 在最后,但必须小于 Ordered.LOWEST_PRECEDENCE
    LogExample(Ordered.LOWEST_PRECEDENCE - 1),

    // 在 log 前面
    SensitiveFilterExample(LogExample.order - 1),

    // 在 敏感词过滤 前面
    PromptEnhancementExample(SensitiveFilterExample.order - 1),

    ;

    private int order;
    }
  • 增加“提示词增强参数”、“用户信息”的Key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public enum ContextKeys {
    // 客户端名称
    ClientName,
    // 敏感词过滤
    SensitiveFilter,
    // 提示词增强控制
    PromptEnhancement,
    // 用户信息
    UserInfo,

    ;
    }
  • 创建PromptEnhancementExampleAdvisor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    @Slf4j
    public class PromptEnhancementExampleAdvisor implements CallAdvisor, StreamAdvisor {

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    return callAdvisorChain.nextCall(enhancement(chatClientRequest));
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    return streamAdvisorChain.nextStream(enhancement(chatClientRequest));
    }

    @Override
    public String getName() {
    return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
    return AdvisorOrders.PromptEnhancementExample.getOrder();
    }

    /**
    * 增强
    * @param chatClientRequest
    * @return
    */
    private ChatClientRequest enhancement(ChatClientRequest chatClientRequest) {
    Object PromptEnhancement = chatClientRequest.context().get(ContextKeys.PromptEnhancement.name());
    if (!Boolean.parseBoolean(StringUtils.toString(PromptEnhancement))) {
    return chatClientRequest;
    }
    UserInfo userInfo = (UserInfo) chatClientRequest.context().get(ContextKeys.UserInfo.name());

    // 假设根据用户相关信息检测到用户是一个小朋友以及提问的类型是大自然
    String sysMsg = "你是一个专门面向小朋友对大自然科普的老师。\n向你提问的是爱好大自然的小朋友,请耐心为解答大自然的问题。\n" + (Objects.isNull(userInfo) ? StringUtils.EMPTY : String.format("小朋友的名字叫:%s", userInfo.name()));
    String userTemplate = "老师你好!我想要问的大自然问题是:%s";
    // 需要异变一个新的 request 设置新的 prompt
    return chatClientRequest.mutate()
    .prompt(chatClientRequest.prompt()
    .augmentSystemMessage(systemMessage -> SystemMessage.builder().text(sysMsg).build())
    .augmentUserMessage(userMessage -> UserMessage.builder().text(String.format(userTemplate, userMessage.getText())).build())
    ).build();
    }
    }
  • 上面这里使用到了chatClientRequest.mutate()异变,其实就是创建了一个新的ChatClientRequest.Builder,拥有原来的Promptcontext,方便快速重构新的ChatClientRequest对象。

总结

  • 三个不同需求功能的Advisor

  • 有使用到上下文进行一些参数共享传递

  • 优先级从高到低的顺序分别是提示词增强 -> 敏感词过滤 -> Log输出

案例执行及结果

创建ChatClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final String SYSTEM_PROMPT = "你是一个Java专家,请帮忙解答提出的Java相关问题。";

@Bean
public ChatClient multiAdvisorClient(DeepSeekChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem(SYSTEM_PROMPT) // 默认系统提示词时Java专家
.defaultAdvisors(spec -> {
spec.params(Map.of(
// 放置客户端名称
ContextKeys.ClientName.name(), "multiAdvisorClient",
// 敏感词过滤默认是false
ContextKeys.SensitiveFilter.name(), false
));

spec.advisors(
new LogExampleAdvisor(), // Log
new PromptEnhancementExampleAdvisor(), // 提示词增强
new SensitiveFilterExampleAdvisor() // 敏感词过滤
);
})
.build();
}

执行及结果

LogExampleAdvisor(Log输出)

  • 执行案例

    1
    2
    3
    4
    5
    6
    7
    public void normalExecutionExample() {
    log.info("\nNormal execution!");
    ChatClientResponse chatClientResponse = multiAdvisorClient.prompt()
    .user("在AI热火朝天的现在,Java的优势是否还存在?")
    .call()
    .chatClientResponse();
    }
  • 请求输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    2025-06-30T10:49:24.397+08:00  INFO 90478 --- [spring-ai-example] [           main] c.s.a.e.a.t.MultiAdvisorClientExample    : 
    Normal execution!
    2025-06-30T11:10:23.089+08:00 INFO 90478 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
    Chat client request to AI
    prompt text -> 你是一个Java专家,请帮忙解答提出的Java相关问题。在AI热火朝天的现在,Java的优势是否还存在?
    context -> {
    "ClientName" : "multiAdvisorClient",
    "SensitiveFilter" : false
    }

  • 响应输出(部分输出省略删除了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    2025-06-30T11:10:54.096+08:00  INFO 90478 --- [spring-ai-example] [           main] c.s.a.e.advisor.three.LogExampleAdvisor  : 
    Chat client response from AI
    output text -> Java 在 AI 时代仍然具有显著优势,尽管新兴语言(如 Python)在 AI 领域更受关注,但 Java 的独特价值体现在以下几个方面:
    ......
    ### **结论**
    Java 在 AI 时代并未过时,而是**定位转换**:从算法研发(Python 主导)转向**生产化部署、大规模系统集成和高性能场景**。两者互补,而非替代。
    context -> {ClientName=multiAdvisorClient, SensitiveFilter=false}


  • 从结果看日志都正常输出了。

PromptEnhancementExampleAdvisor(提示词增强)

  • 执行案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 提示词增强,看看默认的Java提示词是否被修改
    */
    public void promptEnhancementExample() {
    log.info("\nPrompt enhancement execution!");
    ChatClientResponse chatClientResponse = multiAdvisorClient.prompt()
    // 往上下文里传递开启提示词增强
    .advisors(spce -> spce.params(Map.of(
    ContextKeys.PromptEnhancement.name(), true,
    ContextKeys.UserInfo.name(), new UserInfo("大聪明")
    )))
    .user("蜗牛是怎么繁殖的呢?")
    .call()
    .chatClientResponse();
    }
  • 输出的Log

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    2025-06-30T11:17:48.783+08:00  INFO 91208 --- [spring-ai-example] [           main] c.s.a.e.a.t.MultiAdvisorClientExample    : 
    Prompt enhancement execution!
    2025-06-30T11:17:48.893+08:00 INFO 91208 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
    Chat client request to AI
    prompt text -> 你是一个专门面向小朋友对大自然科普的老师。
    向你提问的是爱好大自然的小朋友,请耐心为解答大自然的问题。
    小朋友的名字叫:大聪明老师你好!我想要问的大自然问题是:蜗牛是怎么繁殖的呢?
    context -> {
    "ClientName" : "multiAdvisorClient",
    "UserInfo" : {
    "name" : "大聪明"
    },
    "PromptEnhancement" : true,
    "SensitiveFilter" : false
    }
    2025-06-30T11:18:00.921+08:00 INFO 91208 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
    Chat client response from AI
    output text -> 哇!大聪明小朋友问了一个特别有趣的问题呢!(≧▽≦)

    蜗牛的繁殖方式可神奇啦!它们大部分都是雌雄同体哦,也就是说每只蜗牛既是爸爸又是妈妈~
  • 可以看到输出中的prompt text -> 后面的内容,是被提示词增强后的,System角色的消息和User角色的消息都有被修改了。

SensitiveFilterExampleAdvisor(敏感词过滤)

  • 执行案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 敏感词过滤,看看是否会直接返回、阻断调用
    */
    public void sensitiveFilterExample() {
    log.info("\nSensitive Filter execution!");
    ChatClientResponse chatClientResponse = multiAdvisorClient.prompt()
    // 往上下文里传递开启提示词增强
    .advisors(spce -> spce.params(Map.of(
    ContextKeys.PromptEnhancement.name(), true,
    ContextKeys.UserInfo.name(), new UserInfo("好奇宝子")
    )))
    // 草泥马是敏感词
    .user("草泥马是什么动物啊?")
    .call()
    .chatClientResponse();

    log.info("\nSensitive Filter result text -> \n{}", chatClientResponse.chatResponse().getResult().getOutput().getText());
    log.info("\nSensitive Filter result context -> \n{}", chatClientResponse.context());
    }
  • 输出的Log

    1
    2
    3
    4
    5
    6
    7
    8
    2025-06-30T11:30:11.056+08:00  INFO 91462 --- [spring-ai-example] [           main] c.s.a.e.a.t.MultiAdvisorClientExample    : 
    Sensitive Filter execution!
    2025-06-30T11:30:11.082+08:00 INFO 91462 --- [spring-ai-example] [ main] c.s.a.e.a.t.MultiAdvisorClientExample :
    Sensitive Filter result text ->
    用户输入存在敏感词
    2025-06-30T11:30:11.084+08:00 INFO 91462 --- [spring-ai-example] [ main] c.s.a.e.a.t.MultiAdvisorClientExample :
    Sensitive Filter result context ->
    {UserInfo=UserInfo[name=好奇宝子], ClientName=multiAdvisorClient, PromptEnhancement=true, SensitiveFilter=true}
  • 存在敏感词直接中断未去执行AI通信,仔细看就回发现其实LogExampleAdvisor就已经未执行了。

  • SensitiveFilter=true可以看到上下文也被修改过。

Advisor链

  • 最后这里简单记录一下Advisor链的实现,看看下面这张图

    image

  • 从图里可以看到链是一个双向链表LinkedDeque,且里面是已经排序好的Advisor

  • 除了有我们手动添加的三个Advisor,还有一个是上一篇记录到的默认存在与AI交互通信的Advisor。

  • 另外可以看到调用的是.pop()方法从头部将Advisor弹出进行执行。

最后

  • 从各个案例上看Advisor确实是灵活且强大一个机制,前面有说到与Web的Filter非常相似。
  • 所以过去在Web应用中是如何灵活使用Filter的,也就可以借鉴如何来使用Advisor
  • 接下来是详细学习提示词Prompt相关的使用。
  • 所有案例的源码,都会提交在GitHub上。包:com.spring.ai.example.advisor.three